import {
    Scene,
    Vector3,
    Matrix,
    Space,
    Mesh,
    MeshBuilder,
    Curve3,
    Color3,
    PhysicsAggregate,
    PhysicsShapeType,
    HingeConstraint,
    PhysicsBody,
    HighlightLayer,
    PBRMetallicRoughnessMaterial
} from "@babylonjs/core";
import { getRandomId } from "./utils/randomId";

function createRope(
    scene: Scene,
    size: number,
    position: Vector3,
    rotate: Vector3,
    length: number,
    angle: number,
    id: string
) {
    const diameter = size / 40;
    const height = length / Math.cos(angle);
    const heightGainVector = new Vector3(0, -(length - height / 2), 0);
    const pivotGainVector = new Vector3(0, -height / 2, 0);

    const rope1 = MeshBuilder.CreateCylinder(
        `rope1-${id}`,
        {
            height,
            diameter
        },
        scene
    );
    // 设置枢轴，也就是变换中心点的位置
    rope1.setPivotPoint(pivotGainVector);
    rope1.position = position.add(heightGainVector);
    rope1.rotate(new Vector3(0, 0, -1), angle, Space.LOCAL);

    const rope2 = MeshBuilder.CreateCylinder(
        `rope2-${id}`,
        {
            height,
            diameter
        },
        scene
    );
    rope2.setPivotPoint(pivotGainVector);
    rope2.position = position.add(heightGainVector);
    rope2.rotate(new Vector3(0, 0, -1), -angle, Space.LOCAL);

    return { rope1, rope2 };
}

function createSinglePendulum(
    scene: Scene,
    size: number,
    position: Vector3,
    rotate: Vector3,
    length: number,
    angle: number,
    material: PBRMetallicRoughnessMaterial
) {
    const id = getRandomId();

    // 创建约束（作为合页的轴）
    const hingeBox = MeshBuilder.CreateBox(`hingeBox-${id}`, { size }, scene);
    hingeBox.position = hingeBox.position.add(position);
    // 隐藏
    hingeBox.isVisible = false;

    // 创建球体
    const sphere = MeshBuilder.CreateSphere(`sphere-${id}`, { diameter: size }, scene);
    sphere.position = sphere.position.add(new Vector3(0, -length, 0));
    sphere.position = sphere.position.add(position);
    sphere.material = material;

    // 创建绳索
    const { rope1, rope2 } = createRope(scene, size, position, rotate, length, angle, id);
    rope1.material = material;
    rope2.material = material;
    sphere.addChild(rope1);
    sphere.addChild(rope2);

    const hingeBoxAgg = new PhysicsAggregate(hingeBox, PhysicsShapeType.BOX, { mass: 0 }, scene);
    const sphereAgg = new PhysicsAggregate(
        sphere,
        PhysicsShapeType.SPHERE,
        { mass: 1000, restitution: 1, friction: 0.1 },
        scene
    );

    // 合页约束
    const joint = new HingeConstraint(
        new Vector3(0, 0, 0),
        new Vector3(0, length, 0),
        // @ts-ignore-next-line
        undefined,
        undefined,
        scene
    );
    hingeBoxAgg.body.addConstraint(sphereAgg.body, joint);

    // // 过滤碰撞
    // const FILTER_GROUP_SPHERE = 1;
    // const FILTER_GROUP_HINGE_BOX = 2;
    // hingeBoxAgg.shape.filterMembershipMask = FILTER_GROUP_HINGE_BOX;
    // sphereAgg.shape.filterMembershipMask = FILTER_GROUP_SPHERE;

    // sphereAgg.shape.filterCollideMask = FILTER_GROUP_SPHERE;

    return { sphereAgg, sphere };
}

/**
 * 平滑圆弧
 * @param point1 起始点
 * @param point2 控制点
 * @param point3 终止点
 * @param t 段数
 */
function smoothArc(point1: Vector3, point2: Vector3, point3: Vector3, t: number = 5) {
    return Curve3.CreateQuadraticBezier(point1, point2, point3, t).getPoints();
}

function createStents(
    scene: Scene,
    position: Vector3,
    rotate: Vector3,
    space: number,
    size: number,
    length: number,
    angle: number,
    pendulumNumber: number,
    material: PBRMetallicRoughnessMaterial
) {
    const diameter = size / 4;
    const l = (pendulumNumber - 1) * space + pendulumNumber * size + size;
    const w = length * Math.tan(angle);
    const h = length + 2 * size;
    const radius = size / 2;

    const id = getRandomId();

    const before_top_left = new Vector3(w, 0, -l / 2);
    const before_top_right = new Vector3(w, 0, l / 2);
    const before_bottom_right = new Vector3(w, -h, l / 2);
    const after_bottom_right = new Vector3(-w, -h, l / 2);
    const after_top_right = new Vector3(-w, 0, l / 2);
    const after_top_left = new Vector3(-w, 0, -l / 2);
    const after_bottom_left = new Vector3(-w, -h, -l / 2);
    const before_bottom_left = new Vector3(w, -h, -l / 2);

    const before_top_left_s = smoothArc(
        before_top_left.clone().subtract(new Vector3(0, radius, 0)),
        before_top_left,
        before_top_left.clone().add(new Vector3(0, 0, radius))
    );
    const before_top_right_s = smoothArc(
        before_top_right.clone().subtract(new Vector3(0, 0, radius)),
        before_top_right,
        before_top_right.clone().subtract(new Vector3(0, radius, 0))
    );
    const before_bottom_right_s = smoothArc(
        before_bottom_right.clone().add(new Vector3(0, radius, 0)),
        before_bottom_right,
        before_bottom_right.clone().subtract(new Vector3(radius, 0, 0))
    );
    const after_bottom_right_s = smoothArc(
        after_bottom_right.clone().add(new Vector3(radius, 0, 0)),
        after_bottom_right,
        after_bottom_right.clone().add(new Vector3(0, radius, 0))
    );

    const after_top_right_s = smoothArc(
        after_top_right.clone().subtract(new Vector3(0, radius, 0)),
        after_top_right,
        after_top_right.clone().subtract(new Vector3(0, 0, radius))
    );
    const after_top_left_s = smoothArc(
        after_top_left.clone().add(new Vector3(0, 0, radius)),
        after_top_left,
        after_top_left.clone().subtract(new Vector3(0, radius, 0))
    );
    const after_bottom_left_s = smoothArc(
        after_bottom_left.clone().add(new Vector3(0, radius, 0)),
        after_bottom_left,
        after_bottom_left.clone().add(new Vector3(radius, 0, 0))
    );
    const before_bottom_left_s = smoothArc(
        before_bottom_left.clone().subtract(new Vector3(radius, 0, 0)),
        before_bottom_left,
        before_bottom_left.clone().add(new Vector3(0, radius, 0))
    );

    const path = [
        ...after_bottom_left_s,
        ...before_bottom_left_s,
        ...before_top_left_s,
        ...before_top_right_s,
        ...before_bottom_right_s,
        ...after_bottom_right_s,
        ...after_top_right_s,
        ...after_top_left_s,
        ...after_bottom_left_s
    ];
    // const path = [
    //     ...before_top_left_s,
    //     ...before_top_right_s,
    //     ...before_bottom_right_s,
    //     ...after_bottom_right_s,
    //     ...after_top_right_s,
    //     ...after_top_left_s,
    //     ...after_bottom_left_s,
    //     ...before_bottom_left_s,
    //     ...before_top_left_s
    // ];

    const stents = MeshBuilder.CreateTube(
        `stents-${id}`,
        {
            path,
            radius: diameter / 2,
            sideOrientation: Mesh.DOUBLESIDE, // 双面
            arc: 1
        },
        scene
    );
    stents.position = stents.position.add(new Vector3(0, diameter / 2, 0)).add(position);
    stents.material = material;
    return stents;
}

function createPendulums(
    scene: Scene,
    position: Vector3,
    rotate: Vector3,
    space: number,
    size: number,
    length: number,
    angle: number,
    pendulumNumber: number,
    sphereMaterial: PBRMetallicRoughnessMaterial
) {
    const pendulums = [];
    for (let i = -pendulumNumber / 2; i < pendulumNumber / 2; i++) {
        const { sphere, sphereAgg } = createSinglePendulum(
            scene,
            size,
            position.add(new Vector3(0, 0, (size + space) * i + size / 2)),
            rotate,
            length,
            angle,
            sphereMaterial
        );
        pendulums.push({ sphere, sphereAgg });
    }
    return pendulums;
}

export function createNewtonCradle(
    scene: Scene,
    position: Vector3 = new Vector3(0, 2, 0),
    rotate: Vector3 = new Vector3(0, 0, 0)
) {
    //! 按需调整摆间距，防止碰撞异常
    const space = 0.05;
    const size = 1;
    const length = 4;
    const angle = Math.PI / 6;
    const pendulumNumber = 5;

    // 创建纹理
    // https://doc.babylonjs.com/features/featuresDeepDive/materials/using/introToPBR/#pbrmetallicroughnessmaterial
    const metalMaterial = new PBRMetallicRoughnessMaterial("metalMaterial", scene);
    metalMaterial.baseColor = new Color3(230 / 255, 230 / 255, 250 / 255);
    metalMaterial.metallic = 1; // 金属度
    metalMaterial.roughness = 0.3; // 粗糙度

    // 创建支架
    const stents = createStents(
        scene,
        position,
        rotate,
        space,
        size,
        length,
        angle,
        pendulumNumber,
        metalMaterial
    );

    // 创建摆件
    const pendulums = createPendulums(
        scene,
        position,
        rotate,
        space,
        size,
        length,
        angle,
        pendulumNumber,
        metalMaterial
    );
    // 网格对应的物理网格
    const meshToPhysicsMap: Map<string, PhysicsBody> = pendulums.reduce((acc, cur) => {
        acc.set(cur.sphere.id, cur.sphereAgg.body);
        return acc;
    }, new Map<string, PhysicsBody>());

    // 高亮层
    const highlightLayer = new HighlightLayer("highlightLayer", scene);

    // 鼠标移入物体高亮
    scene.onPointerMove = function () {
        const ray = scene.createPickingRay(
            scene.pointerX,
            scene.pointerY,
            Matrix.Identity(),
            scene.getCameraByName("Camera"),
            false
        );
        const hit = scene.pickWithRay(ray);
        // 先移除所有高亮层
        highlightLayer.removeAllMeshes();
        // 高亮目标物体
        if (hit?.pickedMesh) {
            if (hit.pickedMesh.id.startsWith("sphere")) {
                const mesh = hit.pickedMesh;
                const color = Color3.Green();
                highlightLayer.addMesh(mesh as Mesh, color);
            }
        }
    };

    // 鼠标点击摆件，施力使之摆动
    scene.onPointerDown = function () {
        const ray = scene.createPickingRay(
            scene.pointerX,
            scene.pointerY,
            Matrix.Identity(),
            scene.getCameraByName("Camera"),
            false
        );
        const hit = scene.pickWithRay(ray);

        if (hit?.pickedMesh) {
            const physicsBody = meshToPhysicsMap.get(hit.pickedMesh.id);

            if (physicsBody) {
                physicsBody.applyImpulse(new Vector3(0, 0, -5000), hit.pickedMesh.absolutePosition);

                // 对所有在所选摆之前的，没有摆动的摆件，施加相同的冲量
                for (const physicsBody2 of meshToPhysicsMap.values()) {
                    if (physicsBody2.getLinearVelocity().length() > 1) {
                        continue;
                    }

                    const physicsBodyCenter = physicsBody.getObjectCenterWorld();
                    const physicsBody2Center = physicsBody2.getObjectCenterWorld();

                    if (physicsBody2Center.z < physicsBodyCenter.z) {
                        physicsBody2.applyImpulse(
                            new Vector3(0, 0, -5000),
                            hit.pickedMesh.absolutePosition
                        );
                    }
                }
            }
        }
    };
}
